{
 "cells": [
  {
   "cell_type": "raw",
   "metadata": {
    "vscode": {
     "languageId": "raw"
    }
   },
   "source": [
    "---\n",
    "title: Python integration\n",
    "sidebar_position: 1\n",
    "description: Using Python and Mojo together.\n",
    "---"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Our long-term goal is to make Mojo a *superset of Python* (that is, to make Mojo \n",
    "compatible with existing Python programs). Python programmers should be able to\n",
    "use Mojo immediately, and be able to access the huge ecosystem of Python \n",
    "packages that are available today. \n",
    "\n",
    "However, Mojo is still in early development and many Python features are not yet\n",
    "implemented. You can't currently write everything in Mojo that you can write in\n",
    "Python. And Mojo doesn't have its own ecosystem of packages yet.\n",
    "\n",
    "To help bridge this gap, Mojo lets you import Python modules, call Python \n",
    "functions and interact with Python objects from Mojo code. It runs Python code\n",
    "using a standard Python interpreter (CPython), so your existing Python code\n",
    "doesn't need to change.\n",
    "\n",
    "## Import a Python module\n",
    "\n",
    "To import a Python module in Mojo, just call \n",
    "[`Python.import_module()`](/mojo/stdlib/python/python/Python#import_module) \n",
    "with the module name:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from python import Python\n",
    "\n",
    "fn use_array() raises:\n",
    "    # This is equivalent to Python's `import numpy as np`\n",
    "    var np = Python.import_module(\"numpy\")\n",
    "\n",
    "    # Now use numpy as if writing in Python\n",
    "    var array = np.array([1, 2, 3])\n",
    "    print(array)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[1 2 3]\n"
     ]
    }
   ],
   "source": [
    "use_array()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Yes, this imports Python NumPy, and you can import *any other Python module*\n",
    "that you have installed.\n",
    "\n",
    "A few things to note:\n",
    "\n",
    "- Currently, you cannot import individual members (such as a single Python class\n",
    "  or function)—you must import the whole Python module and then access members\n",
    "  through the module name.\n",
    "\n",
    "- Mojo doesn't yet support top-level code, so the `import_module()` call must\n",
    "  be inside another method. This means you may need to import a module multiple\n",
    "  times or pass around a reference to the module. This works the same way as \n",
    "  Python: importing the module multiple times won't run the initialization\n",
    "  logic more than once, so you don't pay any performance penalty.\n",
    "\n",
    "- `import_module()` may raise an exception (for example, if the module isn't\n",
    "  installed). If you're using it inside an `fn` function, you need to either\n",
    "  handle errors (using a `try/except` clause), or add the `raises` keyword to\n",
    "  the function signature. You'll also see this when calling Python functions\n",
    "  that may raise exceptions. (Raising exceptions is much more common in Python\n",
    "  code than in the Mojo standard library, which \n",
    "  [limits their use for performance reasons](/mojo/roadmap.html#the-standard-library-has-limited-exceptions-use).)\n",
    "\n",
    ":::note\n",
    "\n",
    "Mojo loads the Python interpreter and Python modules at runtime, so\n",
    "wherever you run a Mojo program, it must be able to access a compatible Python\n",
    "interpreter, and to locate any imported Python modules. For more information,\n",
    "see [Python environment](#python-environment).\n",
    "\n",
    ":::"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Import a local Python module\n",
    "\n",
    "If you have some local Python code you want to use in Mojo, just add\n",
    "the directory to the Python path and then import the module.\n",
    "\n",
    "For example, suppose you have a Python file named `mypython.py`:\n",
    "\n",
    "```{.python filename=\"mypython.py\"}\n",
    "import numpy as np\n",
    "\n",
    "def gen_random_values(size, base):\n",
    "    # generate a size x size array of random numbers between base and base+1\n",
    "    random_array = np.random.rand(size, size)\n",
    "    return random_array + base\n",
    "```\n",
    "\n",
    "Here's how you can import it and use it in a Mojo file:\n",
    "\n",
    "```{.mojo filename=\"main.mojo\"}\n",
    "from python import Python\n",
    "\n",
    "fn main() raises:\n",
    "    Python.add_to_path(\"path/to/module\")\n",
    "    var mypython = Python.import_module(\"mypython\")\n",
    "\n",
    "    var values = mypython.gen_random_values(2, 3)\n",
    "    print(values)\n",
    "```\n",
    "\n",
    "Both absolute and relative paths work with \n",
    "[`add_to_path()`](/mojo/stdlib/python/python/Python#add_to_path). For example, you\n",
    "can import from the local directory like this:\n",
    "\n",
    "```mojo\n",
    "Python.add_to_path(\".\")\n",
    "```\n",
    "\n",
    "## Call Mojo from Python\n",
    "\n",
    "As shown above, you can call out to Python modules from Mojo. However, there's \n",
    "currently no way to do the reverse—import Mojo modules from Python or call Mojo\n",
    "functions from Python.\n",
    "\n",
    "This may present a challenge for using certain modules. For example, many UI \n",
    "frameworks have a main event loop that makes callbacks to user-defined code\n",
    "in response to UI events. This is sometimes called an \"inversion of control\" \n",
    "pattern. Instead of your application code calling *in* to a library, the \n",
    "framework code calls *out* to your application code.\n",
    "\n",
    "This pattern doesn't work because you can't pass Mojo callbacks to a Python \n",
    "module.\n",
    "\n",
    "For example, consider the popular [Tkinter package](https://docs.python.org/3/library/tkinter.html). \n",
    "The typical usage for Tkinter is something like this:\n",
    "\n",
    "- You create a main, or \"root\" window for the application.\n",
    "- You add one or more UI widgets to the window. The widgets can have associated\n",
    "  callback functions (for example, when a button is pushed).\n",
    "- You call the root window's `mainloop()` method, which listens for events, \n",
    "  updates the UI, and invokes callback functions. The main loop keeps running\n",
    "  until the application exits.\n",
    "\n",
    "Since Python can't call back into Mojo, one alternative is to have the Mojo\n",
    "application drive the event loop and poll for updates. The following example\n",
    "uses Tkinter, but the basic approach can be applied to other packages.\n",
    "\n",
    "First we create a Python module that defines a Tkinter interface, with a window\n",
    "and single button:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%python\n",
    "import tkinter as tk\n",
    "\n",
    "class App:\n",
    "    def __init__(self):\n",
    "        self._root = tk.Tk()\n",
    "        self.clicked = False\n",
    "\n",
    "    def click(self):\n",
    "        self.clicked = True\n",
    "\n",
    "    def create_button(self, button_text: str):\n",
    "        button = tk.Button(\n",
    "            master=self._root,\n",
    "            text=button_text,\n",
    "            command=self.click\n",
    "        )\n",
    "        button.place(relx=0.5, rely=0.5, anchor=tk.CENTER)\n",
    "\n",
    "    def create(self, res: str):\n",
    "        self._root.geometry(res)\n",
    "        self.create_button(\"Hello Mojo!\")\n",
    "\n",
    "    def update(self):\n",
    "        self._root.update()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can call this module from Mojo like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "from python import Python\n",
    "\n",
    "fn button_clicked():\n",
    "    print(\"Hi from a Mojo🔥 fn!\")\n",
    "\n",
    "def main():\n",
    "    Python.add_to_path(\".\")\n",
    "    var app = Python.import_module(\"myapp\").App()\n",
    "    app.create(\"800x600\")\n",
    "\n",
    "    while True:\n",
    "        app.update()\n",
    "        if app.clicked:\n",
    "            button_clicked()\n",
    "            app.clicked = False"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Instead of the Python module calling the Tkinter `mainloop()` method, the Mojo \n",
    "code calls the `update()` method in a loop and checks the `clicked` attribute \n",
    "after each update.\n",
    "\n",
    "## Python environment\n",
    "\n",
    "The Mojo SDK depends on an existing Python dynamic library. At runtime, Mojo\n",
    "uses the first Python in the search path (`PATH`), to find an associated dynamic\n",
    "Python library of the same version. This will also add any modules from the\n",
    "activated virtual environment. \n",
    "\n",
    "### Resolving issues\n",
    "\n",
    "Finding libpython may fail if the Python interpreter on top of `PATH` does not\n",
    "have an associated dynamic library. Some Python distributions don't include the\n",
    "shared library, and others only have a static library which isn't supported by\n",
    "Mojo yet.\n",
    "\n",
    "You can find a compatible Python on your system by running this Python script:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "python"
    }
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import subprocess\n",
    "\n",
    "FIND_LIBPYTHON = \"\"\"\n",
    "import os\n",
    "import sys\n",
    "from pathlib import Path\n",
    "from sysconfig import get_config_var\n",
    "ext = \"dll\" if os.name == \"nt\" else \"dylib\" if sys.platform == \"darwin\" else \"so\"\n",
    "binary = f\"libpython{get_config_var('py_version_short')}.{ext}\"\n",
    "for folder in [Path(get_config_var(p)) for p in [\"LIBPL\", \"LIBDIR\"]]:\n",
    "    libpython_path = folder / binary\n",
    "    if libpython_path.is_file():\n",
    "        print(libpython_path.resolve())\n",
    "        exit(0)\n",
    "exit(1)\n",
    "\"\"\"\n",
    "FIND_PYTHON_VER = \"import sysconfig; print(sysconfig.get_python_version())\"\n",
    "\n",
    "exe_names = [\"python3\", \"python\"] + [f\"python3.{i}\" for i in range(8, 13)]\n",
    "seen = []\n",
    "executables = []\n",
    "\n",
    "print(\"Mojo will attempt to use the first python executable from the top:\\n\")\n",
    "print(\"vers | compat | path\")\n",
    "for path in os.environ[\"PATH\"].split(\":\"):\n",
    "    for exe in exe_names:\n",
    "        full_path = os.path.join(path, exe)\n",
    "        if os.path.exists(full_path):\n",
    "            pyver = subprocess.check_output([full_path, \"-c\", FIND_PYTHON_VER], text=True).strip()\n",
    "            res = subprocess.run([full_path, \"-c\", FIND_LIBPYTHON], text=True, capture_output=True)\n",
    "            libpython = res.stdout.strip()\n",
    "            if res.returncode != 0:\n",
    "                print(f\"{pyver:<7} no      {full_path}\")\n",
    "            elif libpython not in seen:\n",
    "                print(f\"{pyver:<7} yes     {full_path}\")\n",
    "                seen.append(libpython)\n",
    "                executables.append(full_path)\n",
    "\n",
    "if not executables:\n",
    "    print(\"no compatible Python environments found\")\n",
    "else:\n",
    "    print(\"\\ncreate and activate a virtual environment to use a different Python version:\")\n",
    "    print(f\"    {executables[-1]} -m venv .venv\")\n",
    "    print(\"    source .venv/bin/activate\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Which will produce output like:\n",
    "\n",
    "```output\n",
    "Mojo will attempt to use the first python executable from the top:\n",
    "\n",
    "vers | compat | path\n",
    "3.11    yes     /opt/homebrew/opt/python@3.11/libexec/bin/python3\n",
    "3.12    yes     /opt/homebrew/bin/python3\n",
    "3.9     yes     /usr/bin/python3\n",
    "\n",
    "create and activate a virtual environment to use a different Python version:\n",
    "    /usr/bin/python3 -m venv .venv\n",
    "    source .venv/bin/activate\n",
    "```\n",
    "\n",
    "If you have no compatible environment, you can install a compatible version of\n",
    "Python that includes shared libraries. Try following the instructions in [Set up\n",
    "a Python environment with Conda](#set-up-a-python-environment-with-conda) to\n",
    "install a virtual environment. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Set up a Python environment with Conda\n",
    "\n",
    "Using a Python virtual environment such as \n",
    "[Conda](https://docs.conda.io/en/latest/) is one way to get a version of Python\n",
    "that will work reliably with Mojo. It comes with the required dynamic library,\n",
    "and ensures there are no conflicts with system dependencies. \n",
    "\n",
    "To set up a virtual environment with Conda:\n",
    "\n",
    "1. Install Conda by following the \n",
    "   [Quick command-line install instructions](https://docs.conda.io/projects/miniconda/en/latest/#quick-command-line-install).\n",
    "\n",
    "2. Initialize Conda for all the shells on your path:\n",
    "\n",
    "   ```bash\n",
    "   ~/miniconda3/bin/conda init --all\n",
    "   ```\n",
    "\n",
    "   Or just one at a time:\n",
    "\n",
    "   ```bash\n",
    "   ~/miniconda3/bin/conda init zsh\n",
    "   ```\n",
    "\n",
    "2. Restart your shell. \n",
    "\n",
    "3. Install your desired version of Python and activate the environment:\n",
    "\n",
    "    ```bash\n",
    "    conda create -n 3.10 python=3.10\n",
    "    conda activate 3.10\n",
    "    ```\n",
    "\n",
    "After setting up the Conda virtual environment, you can install any Python \n",
    "packages you want to use with Mojo, with `conda install` or `pip install`. For\n",
    "example:\n",
    "\n",
    "```bash\n",
    "conda install numpy\n",
    "pip install pillow\n",
    "```\n",
    "\n",
    "Now whenever you `conda activate 3.10`, Mojo will be able to find any modules\n",
    "you installed into that environment.\n",
    "\n",
    "For more information on using Conda with Mojo, see \n",
    "[Using Mojo with Python](https://www.modular.com/blog/using-mojo-with-python) on\n",
    "the Modular Blog."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Mojo",
   "language": "mojo",
   "name": "mojo-jupyter-kernel"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "mojo"
   },
   "file_extension": ".mojo",
   "mimetype": "text/x-mojo",
   "name": "mojo"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
